---
sidebar_position: 9
---

# Шаг 7. Сбор метрик с помощью Prometheus

[**Метрика**](https://opentelemetry.io/docs/concepts/signals/metrics/) — это измерение сервиса, полученное во время выполнения. Момент записи измерений известен как событие метрики, которое состоит не только из самого измерения, но также времени, в которое оно было записано, и связанных с ним метаданных.

Метрики приложений и запросов являются важными показателями доступности и производительности. Пользовательские метрики могут дать представление о том, как показатели доступности влияют на опыт пользователей или бизнес. Собранные данные могут быть использованы для оповещения об отключении или принятия решений по планированию для автоматического расширения развертывания при высоком спросе.

[API OpenTelemetry метрик](https://opentelemetry-python.readthedocs.io/en/latest/api/metrics.html) описывает классы, используемые для генерации метрик. `MeterProvider` предоставляет пользователям доступ к `Meter`, который, в свою очередь, используется для создания объектов, фиксирующие метрики. Таким образом, OpenTelemetry предоставляет инструменты для сборки метрик из коробки.

[**Prometheus**](https://prometheus.io/) — это система мониторинга и оповещений, хранящая и обрабатывающая метрики, собираемые из экспортеров в Time Series Database (TSDB). В отличие от SQl-like СУБД, Prometheus сам собирает метрики по указанным хостам.

В Python есть свой клиент для сборки метрик. Однако мы попробуем использовать метрики, которые собираются с помощью OpenTelemetry, и вернемся через интерфейс экспортера **Prometheus**.  Делать это будем, опираясь на [официальную документацию](https://opentelemetry.io/docs/instrumentation/python/exporters/#prometheus).

Если вы используете Prometheus для сбора данных метрик, вам необходимо сначала настроить его.

Сначала создайте конфигурационный файл `prometheus.yml` в директории `prometheus`. Файл `prometheus/prometheus.yml` выглядит следующим образом:

```yaml
scrape_configs:
  - job_name: 'otel-iu5devops-app'
    scrape_interval: 5s
    static_configs:
      - targets: ['iu5devops-app:5000']

```

Затем установите пакет Prometheus exporter:

```bash
pip install opentelemetry-exporter-prometheus
```

Не забываем обновлять файл `app/requirements.txt`:

```text
Flask==3.0.0
opentelemetry-sdk==1.21.0
opentelemetry-exporter-prometheus==1.12.0rc1
opentelemetry-exporter-otlp-proto-grpc==1.21.0
```

Затем мы настроим экспортер при инициализации показателей. Также воспользуемся [примером из документации SDK](https://opentelemetry-python.readthedocs.io/en/latest/api/metrics.html#opentelemetry.metrics.Meter.create_observable_counter), который счетчик для периодического отображения системного процессорного времени. Не забудем поддержать [метод-экспортер `/metrics`](https://dev.to/camptocamp-ops/implement-prometheus-metrics-in-a-flask-application-p18) для отдачи метрик Prometheus.

В конечном итоге файл `app/app.py` будет выглядеть следующим образом:

```python
import os
# highlight-start
from typing import Iterable
from prometheus_client import generate_latest
# highlight-end

from flask import Flask
from random import randint
# highlight-next-line
from opentelemetry import trace, metrics
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
# highlight-start
from opentelemetry.metrics import get_meter, Observation, CallbackOptions
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.prometheus import PrometheusMetricReader
# highlight-end
from opentelemetry.sdk.resources import SERVICE_NAME, Resource

resource = Resource.create({SERVICE_NAME: os.environ.get(
    'APP_SERVICE_NAME', "my-python-service")})

tracer_provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(OTLPSpanExporter(
    endpoint=os.environ.get('TRACE_ENDPOINT', "http://localhost:4317")))
tracer_provider.add_span_processor(processor)
trace.set_tracer_provider(tracer_provider)
tracer = trace.get_tracer(__name__)

# highlight-start
reader = PrometheusMetricReader()
meter_provider = MeterProvider(resource=resource, metric_readers=[reader])
metrics.set_meter_provider(meter_provider)
# highlight-end

app = Flask(__name__)


def roll():
    return randint(1, 6)


@app.route("/rolldice")
def roll_dice():
    with tracer.start_as_current_span("server_request"):
        return str(roll())


# highlight-start
@app.route('/metrics')
def metrics():
    return generate_latest()


def cpu_time_callback(options: CallbackOptions) -> Iterable[Observation]:
    observations = []
    with open("/proc/stat") as procstat:
        procstat.readline()  # skip the first line
        for line in procstat:
            if not line.startswith("cpu"):
                break
            cpu, *states = line.split()
            observations.append(Observation(
                int(states[0]) // 100, {"cpu": cpu, "state": "user"}))
            observations.append(Observation(
                int(states[1]) // 100, {"cpu": cpu, "state": "nice"}))
            observations.append(Observation(
                int(states[2]) // 100, {"cpu": cpu, "state": "system"}))
    return observations


meter = get_meter("example-meter")
counter = meter.create_counter("example-counter")
meter.create_observable_counter(
    "system.cpu.time",
    callbacks=[cpu_time_callback],
    unit="s",
    description="CPU time"
)
# highlight-end


if __name__ == "__main__":
    host = os.environ.get('APP_HOST_NAME', "0.0.0.0")
    port = int(os.environ.get('APP_PORT', 5000))
    app.run(debug=True, host=host, port=port)

```

Также мы укажем в `docker-compose.yaml` то, как поднимать Prometheus:

```yaml
services:
  app:
    image: iu5devops/app
    build:
      context: ./app
      dockerfile: Dockerfile
    container_name: iu5devops-app
    networks:
      - iu5devops
    ports:
      - 8080:5000
    environment:
      - APP_SERVICE_NAME=iu5devops-app
      - TRACE_ENDPOINT=http://iu5devops-jaeger:4317

  jaeger:
    image: jaegertracing/all-in-one
    container_name: iu5devops-jaeger
    networks:
      - iu5devops
    ports:
      - 16686:16686

# highlight-start
  prometheus:
    image: prom/prometheus
    container_name: iu5devops-prometheus
    networks:
      - iu5devops
    ports:
      - 9090:9090
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus

volumes:
  prometheus-data: {}
# highlight-end

networks:
  iu5devops: {}

```

Соберите контейнер снова и запустите полученную конфигурацию:

```bash
docker compose down
docker compose build
docker compose up -d
```

Теперь у нас стал доступен графический интерфейс Prometheus ([http://localhost:9090/](http://localhost:9090/)).

Сначала проверьте, что экспортер нашего приложения работает нормально. Откройте в браузере [http://localhost:8080/metrics](http://localhost:8080/metrics) в своем веб-браузере. Вы можете увидеть, например, следующее:

<details>
  <summary>Просмотр примера выходных данных</summary>
  <div>

```text
# HELP python_gc_objects_collected_total Objects collected during gc
# TYPE python_gc_objects_collected_total counter
python_gc_objects_collected_total{generation="0"} 2177.0
python_gc_objects_collected_total{generation="1"} 735.0
python_gc_objects_collected_total{generation="2"} 0.0
# HELP python_gc_objects_uncollectable_total Uncollectable objects found during GC
# TYPE python_gc_objects_uncollectable_total counter
python_gc_objects_uncollectable_total{generation="0"} 0.0
python_gc_objects_uncollectable_total{generation="1"} 0.0
python_gc_objects_uncollectable_total{generation="2"} 0.0
# HELP python_gc_collections_total Number of times this generation was collected
# TYPE python_gc_collections_total counter
python_gc_collections_total{generation="0"} 110.0
python_gc_collections_total{generation="1"} 10.0
python_gc_collections_total{generation="2"} 0.0
# HELP python_info Python platform information
# TYPE python_info gauge
python_info{implementation="CPython",major="3",minor="12",patchlevel="0",version="3.12.0"} 1.0
# HELP process_virtual_memory_bytes Virtual memory size in bytes.
# TYPE process_virtual_memory_bytes gauge
process_virtual_memory_bytes 1.653633024e+09
# HELP process_resident_memory_bytes Resident memory size in bytes.
# TYPE process_resident_memory_bytes gauge
process_resident_memory_bytes 5.656576e+07
# HELP process_start_time_seconds Start time of the process since unix epoch in seconds.
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1.70008076238e+09
# HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.
# TYPE process_cpu_seconds_total counter
process_cpu_seconds_total 2.62
# HELP process_open_fds Number of open file descriptors.
# TYPE process_open_fds gauge
process_open_fds 11.0
# HELP process_max_fds Maximum number of open file descriptors.
# TYPE process_max_fds gauge
process_max_fds 1.048576e+06
# HELP system_cpu_time_s_total CPU time
# TYPE system_cpu_time_s_total counter
system_cpu_time_s_total{cpu="cpu0",state="user"} 36.0
system_cpu_time_s_total{cpu="cpu0",state="nice"} 0.0
system_cpu_time_s_total{cpu="cpu0",state="system"} 26.0
system_cpu_time_s_total{cpu="cpu1",state="user"} 39.0
system_cpu_time_s_total{cpu="cpu1",state="nice"} 0.0
system_cpu_time_s_total{cpu="cpu1",state="system"} 29.0
system_cpu_time_s_total{cpu="cpu2",state="user"} 47.0
system_cpu_time_s_total{cpu="cpu2",state="nice"} 0.0
system_cpu_time_s_total{cpu="cpu2",state="system"} 28.0
system_cpu_time_s_total{cpu="cpu3",state="user"} 41.0
system_cpu_time_s_total{cpu="cpu3",state="nice"} 0.0
system_cpu_time_s_total{cpu="cpu3",state="system"} 27.0
system_cpu_time_s_total{cpu="cpu4",state="user"} 38.0
system_cpu_time_s_total{cpu="cpu4",state="nice"} 0.0
system_cpu_time_s_total{cpu="cpu4",state="system"} 25.0
system_cpu_time_s_total{cpu="cpu5",state="user"} 36.0
system_cpu_time_s_total{cpu="cpu5",state="nice"} 0.0
system_cpu_time_s_total{cpu="cpu5",state="system"} 25.0
system_cpu_time_s_total{cpu="cpu6",state="user"} 36.0
system_cpu_time_s_total{cpu="cpu6",state="nice"} 0.0
system_cpu_time_s_total{cpu="cpu6",state="system"} 27.0
system_cpu_time_s_total{cpu="cpu7",state="user"} 47.0
system_cpu_time_s_total{cpu="cpu7",state="nice"} 0.0
system_cpu_time_s_total{cpu="cpu7",state="system"} 26.0
system_cpu_time_s_total{cpu="cpu8",state="user"} 38.0
system_cpu_time_s_total{cpu="cpu8",state="nice"} 0.0
system_cpu_time_s_total{cpu="cpu8",state="system"} 27.0
system_cpu_time_s_total{cpu="cpu9",state="user"} 37.0
system_cpu_time_s_total{cpu="cpu9",state="nice"} 0.0
system_cpu_time_s_total{cpu="cpu9",state="system"} 25.0
```

  </div>
</details>

Если метрика отдается через экспортер, то самое время проверить как дела на принимающей стороне. Откройте в браузере UI Prometheus [http://localhost:9090/](http://localhost:9090/) и перейдите через верхнее меню на страницу `Targets` (`Status>Targets`). Если все в порядке, то статус цели нашего endpoint-а нашего приложения будет `up`:

![Targets Prometheus](images/prometheus-targets-up.png)

После нашего успеха, стоит рассмотреть собираемые метрики. Основы синтаксиса запросов Prometheus вы изучить в [официальной документации](https://prometheus.io/docs/prometheus/latest/querying/basics/). Вернитесь на главную страницу Prometheus:

![Prometheus query example](images/prometheus-query-example1.png)

Выполните несколько запросов:

![Prometheus query example](images/prometheus-query-example2.png)

![Prometheus query example](images/prometheus-query-example3.png)
